package org.dreamwork.modbus.tcp;

import org.dreamwork.concurrent.Looper;
import org.dreamwork.modbus.ModbusException;
import org.dreamwork.modbus.Predefined;
import org.dreamwork.modbus.protocol.PDU;
import org.dreamwork.modbus.tcp.protocol.MBAP;
import org.dreamwork.modbus.tcp.protocol.TcpADU;
import org.dreamwork.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static org.dreamwork.modbus.Predefined.Function.*;

@SuppressWarnings ("unused")
public class Client implements Closeable {
    private static final AtomicLong counter = new AtomicLong (0);

    private final Lock locker = new ReentrantLock (true);
    private final Logger logger = LoggerFactory.getLogger (Client.class);

    private char transaction = 0;

    private Socket socket;
    private String host;
    private int port = Server.DEFAULT_PORT;
    private boolean autoReconnect = true, reuseAddress = true;
    /** 是否模拟 PLC 寻址方式 */
    private boolean simulatePLCAddressing = true;
    private int soTimeout = 300_000;    // 300 seconds, or 5 minutes.
    private IClientListener listener;

    private String loopName;

    private OutputStream out;
    private InputStream  in;

    public Client () {}

    public Client (String host) {
        setHost (host);
    }

    public Client (String host, int port) {
        setHost (host);
        setPort (port);
    }

    public String getHost () {
        return host;
    }

    public void setHost (String host) {
        this.host = host;
    }

    public int getPort () {
        return port;
    }

    public void setPort (int port) {
        this.port = port;
    }

    public boolean isAutoReconnect () {
        return autoReconnect;
    }

    public Client setAutoReconnect (boolean autoReconnect) {
        this.autoReconnect = autoReconnect;
        return this;
    }

    public Client setReuseAddress (boolean on) {
        this.reuseAddress = on;
        return this;
    }

    public IClientListener getListener () {
        return listener;
    }

    public Client setListener (IClientListener listener) {
        this.listener = listener;
        return this;
    }

    public Client setSoTimeout (int timeout) {
        this.soTimeout = timeout;
        return this;
    }

    /**
     * 是否模拟 PLC 寻址方式
     * @return true 表示模拟 PLC 的分类寻址方式；否则返回 false
     */
    public boolean isSimulatePLCAddressing () {
        return simulatePLCAddressing;
    }

    public Client setSimulatePLCAddressing (boolean simulatePLCAddressing) {
        this.simulatePLCAddressing = simulatePLCAddressing;
        return this;
    }

    public Client connect () throws IOException {
        if (host == null) throw new NullPointerException ("host");
        socket = new Socket (host, port);
        socket.setReuseAddress (reuseAddress);
        socket.setSoTimeout (soTimeout);
        out = socket.getOutputStream ();
        in  = socket.getInputStream ();

        long index = counter.getAndIncrement ();
        loopName = "client." + index;
        Looper.create (loopName, 16, 1);
        if (listener != null) {
            Looper.runInLoop (loopName, ()-> listener.onConnected (host, port));
        }

        return this;
    }

    public Client connect (String host) throws IOException {
        setHost (host);
        return connect ();
    }

    public Client connect (String host, int port) throws IOException {
        setPort (port);
        return connect (host);
    }

    public void disconnect () {
        closeConnect ();
        Looper.destory (loopName);
    }

    /**
     * 读线圈状态
     * @param address 线圈状态起始地址
     * @param count   读取线圈数量
     * @return 线圈状态
     * @throws IOException 任何io异常
     * @throws ModbusException modbus 异常
     */
    public boolean[] readCoils (int address, int count) throws IOException, ModbusException {
        return readBoolean (Predefined.Function.Read_Coils, address, count);
    }

    /**
     * 读离散量状态
     * @param address 离散量状态起始地址
     * @param count   读取离散量数量
     * @return 离散量状态
     * @throws IOException 任何io异常
     * @throws ModbusException modbus 异常
     */
    public boolean[] readDiscreteInputs (int address, int count) throws IOException, ModbusException {
        return readBoolean (Read_Discrete_Inputs, address, count);
    }

    /**
     * 读输入寄存器
     * @param address 起始地址
     * @param count   读取的数量
     * @return 寄存器的值
     * @throws IOException 任何 io 异常
     * @throws ModbusException modbus 异常
     */
    public int[] readInputRegisters (int address, int count) throws IOException, ModbusException {
        return readInt (Read_Input_Registers, address, count);
    }

    /**
     * 读保持寄存器
     * @param address 起始地址
     * @param count   读取的数量
     * @return 寄存器的值
     * @throws IOException 任何 io 异常
     * @throws ModbusException modbus 异常
     */
    public int[] readHoldingRegisters (int address, int count) throws IOException, ModbusException {
        return readInt (Read_Holding_Registers, address, count);
    }

    /**
     * 写单个线圈
     * @param address 线圈地址
     * @param value   线圈状态
     * @throws IOException 任何 io 异常
     * @throws ModbusException modbus 异常
     */
    public void writeSingleCoil (int address, boolean value) throws IOException, ModbusException {
        // 根据 modbus 协议，线圈值为 真 时，对应的int值为 0xff00
        requireSingleWriteAndCheck (Write_Single_Coil, address, value ? 0xff00 : 0);
    }

    /**
     * 写单个寄存器
     * @param address 寄存器地址
     * @param value   寄存器值
     * @throws IOException 任何 io 异常
     * @throws ModbusException modbus 异常
     */
    public void writeSingleRegister (int address, int value) throws IOException, ModbusException {
        requireSingleWriteAndCheck (Write_Single_Register, address, value);
    }

    /**
     * 写多个线圈
     * @param address 线圈起始地址
     * @param values  线圈状态
     * @throws IOException 任何 io 异常
     * @throws ModbusException 任何 modbus 异常
     */
    public void writeMultipleCoils (int address, boolean[] values) throws IOException, ModbusException {
        requireMultipleWriteAndCheck (Write_Multiple_Coils, address, values.length, Helper.bool2byte (values));
    }

    /**
     * 写多个寄存器
     * @param address 寄存器起始地址
     * @param values  寄存器值
     * @throws IOException 任何 io 异常
     * @throws ModbusException 任何 modbus 异常
     */
    public void writeMultipleRegisters (int address, int[] values) throws IOException, ModbusException {
        requireMultipleWriteAndCheck (Write_Multiple_Registers, address, values.length, Helper.int2byte (values));
    }

    private void requireSingleWriteAndCheck (int function, int address, int value) throws IOException, ModbusException {
        locker.lock ();
        try {
            checkConnection ();
            TcpADU adu = createADU ();
            PDU pdu = new PDU ();
            pdu.function = function;
            pdu.address  = address;
            pdu.value    = value;
            adu.pdu      = pdu;
            out.write (adu.toByteArray ());
            out.flush ();

            parseResult (address);
        } catch (IOException ex) {
            closeConnect ();
            throw ex;
        } finally {
            locker.unlock ();
        }
    }

    private void closeConnect () {
        if (socket != null) try {
            socket.close ();
            socket = null;
            if (listener != null) {
                Looper.runInLoop (loopName, () -> listener.onDisconnected (host, port));
            }
        } catch (IOException ex) {
            // ignore
        }
    }

    public void requireMultipleWriteAndCheck (int function, int address, int count, byte[] values) throws IOException, ModbusException {
        locker.lock ();
        try {
            checkConnection ();
            TcpADU adu = createADU ();
            PDU pdu = new PDU ();
            pdu.function = function;
            pdu.address  = address;
            pdu.count    = count;
            pdu.data     = values;
            adu.pdu      = pdu;
            out.write (adu.toByteArray ());
            out.flush ();

            parseResult (address);
        } catch (IOException ex) {
            closeConnect ();
            throw ex;
        } finally {
            locker.unlock ();
        }
    }

    private boolean[] readBoolean (int function, int address, int count) throws IOException, ModbusException {
        byte[] buff = requireReadAndCheck (function, address, count);
        int length  = buff[8] & 0xff;
        return Helper.byte2bool (buff, 9, length, count);
    }

    private int[] readInt (int function, int address, int count) throws IOException, ModbusException {
        byte[] buff = requireReadAndCheck (function, address, count);
        int length  = buff[8] & 0xff;
        return Helper.byte2int (buff, 9, length);
    }

    private byte[] requireReadAndCheck (int function, int address, int count) throws IOException, ModbusException {
        locker.lock ();
        try {
            checkConnection ();
            TcpADU adu = createADU ();
            PDU pdu = new PDU ();
            pdu.function = function;
            pdu.count    = count;
            pdu.address  = determineAddress (function, address);
            adu.pdu      = pdu;
            out.write (adu.toByteArray ());
            out.flush ();

            if (logger.isTraceEnabled ()) {
                logger.trace ("sending request: \r\n{}", StringUtil.format (adu.toByteArray ()));
            }

            return parseResult (address);
        } catch (IOException ex) {
            closeConnect ();
            throw ex;
        } finally {
            locker.unlock ();
        }
    }

    private byte[] parseResult (int rawAddress) throws ModbusException, IOException {
        byte[] raw = Helper.readRawPacket (in);
        if (logger.isTraceEnabled ()) {
            logger.trace ("received response:\r\n{}", StringUtil.format (raw));
        }
        int code = raw[7] & 0xff;           // 跳过 7 字节 modbus-tcp 头, 获取服务端响应码
        if (code > 0x80) {                  // 服务器响应了一个错误消息
            final ModbusException me = new ModbusException (raw[8] & 0xff, rawAddress);
            if (listener != null) {
                Looper.runInLoop (loopName, () -> listener.onError (host, port, me));
            }
            throw me;
        }
        return raw;
    }

    private void checkConnection () throws IOException {
        if (socket == null || socket.isClosed ()) {
            if (autoReconnect) {
                connect ();
            } else {
                throw new IOException ("socket is closed.");
            }
        }
    }

    private TcpADU createADU () {
        locker.lock ();
        int id;
        try {
            id = transaction ++;
            if (id > 0xff) {
                id = transaction = 0;
            }
        } finally {
            locker.unlock ();
        }
        TcpADU adu = new TcpADU ();
        adu.header = new MBAP ();
        adu.header.transaction = id;


        return adu;
    }

    /**
     * 计算真正的寄存器/线圈地址
     * <p>
     * 若开启了 {@code PLC分组寻址}，则按照以下规则进行寻址
     * <table border='1'>
     * <tr><th>功能码</th><th>含义</th><th>寻址</th></tr>
     * <tr><td>0x01</td><td>读线圈</td><td>原始地址</td></tr>
     * <tr><td>0x02</td><td>读离散输入</td><td>1xxxx</td></tr>
     * <tr><td>0x03</td><td>读保持寄存器</td><td>4xxxx</td></tr>
     * <tr><td>0x06</td><td>写保持寄存器</td><td>4xxxx</td></tr>
     * <tr><td>0x10</td><td>批量写保持寄存器</td><td>4xxxx</td></tr>
     * <tr><td>0x04</td><td>读输入寄存器</td><td>3xxxx</td></tr>
     * </table>
     * 当分组地址非原始地址时，且传入的地址超出本分组的起始地址时，将地址减去本组起始地址，例：<pre>
     * function = 0x03;         // 读保持寄存器
     * address = 40123;         // 传入的地址
     * </pre> 则计算后的结果为 <pre>
     * address = address - 40001 = 122
     * </pre>
     * 若未开启 {@code PLC分组寻址}，则不做任何计算直接返回原地址值
     * @param function modbus 功能码
     * @param address  起始地址
     * @return 计算后的真正地址
     */
    private int determineAddress (int function, int address) {
        if (simulatePLCAddressing) {
            switch (function) {
                case Read_Discrete_Inputs:
                    if (address >= 10001) {
                        return address - 10001;
                    }
                case Read_Holding_Registers:
                case Write_Single_Register:
                case Write_Multiple_Registers:
                    if (address >= 40001) {
                        return address - 40001;
                    }
                case Read_Input_Registers:
                    if (address >= 30001) {
                        return address - 30001;
                    }
            }
        }
        return address;
    }

    @Override
    public void close () {
        disconnect ();
    }
}